import { isArray, isBuffer, isString, once } from 'lodash-es'
import path, { posix as posixPath } from 'path'
import type { Awaitable, EmitAssetArgs } from '../types'
import {
  CWD,
  doHash,
  esm_default,
  getExtname,
  isPromiseLike,
  loadDependency,
  makeRelativeLegalPosixPath,
  normalizePath,
  toBuffer,
  _require
} from '../utils'

export const WEBPACK_PUBLIC_PATH = '__complugin_public_path__'
export const PLUGIN_KEY = '$complugin'

export const TRANSFORM_LOADER = path.resolve(__dirname, 'webpack/loaders/transform.js')
export const Load_INFO_LOADER = path.resolve(__dirname, 'webpack/loaders/loadInfo.js')

export type WebpackLoaderContext = ThisParameterType<import('webpack').LoaderDefinition>

class PolyfillSource {
  private _valueIsBuffer: boolean
  private _value: string | Buffer | undefined
  private _valueAsBuffer: Buffer | undefined

  constructor(value: string | Buffer, convertToString = false) {
    const isBuf = isBuffer(value)
    if (isString(value)) {
      value = String(value)
    } else if (value instanceof Uint8Array) {
      value = toBuffer(value)
    } else {
      throw new TypeError("argument 'value' must be either string of Buffer")
    }

    this._valueIsBuffer = !convertToString && isBuf
    this._value = convertToString && isBuf ? undefined : value
    this._valueAsBuffer = (isBuf ? value : undefined) as any
  }

  isBuffer() {
    return this._valueIsBuffer
  }

  source() {
    if (this._value == null) {
      this._value = this._valueAsBuffer?.toString() ?? ''
    }
    return this._value
  }

  buffer() {
    if (this._valueAsBuffer === undefined) {
      this._valueAsBuffer = toBuffer(this._value ?? '')
    }
    return this._valueAsBuffer
  }

  size() {
    return this.buffer().length
  }

  map(options?: any) {
    return null
  }

  sourceAndMap(options?: any) {
    return {
      source: this.source(),
      map: this.map(options)
    }
  }
}

export const toWebpackSource = (
  source: string | Uint8Array,
  webpack: typeof import('webpack') = loadDependency('webpack', true)
): import('webpack').sources.RawSource => {
  const RawSource = webpack.sources?.RawSource ?? PolyfillSource

  return new RawSource(typeof source === 'string' ? source : toBuffer(source)) as any
}

export const interpolateAssetName = (
  name: string,
  source: string | Uint8Array | { code: string | Uint8Array },
  assetNames: import('webpack').Compilation['outputOptions']['assetModuleFilename'] = 'assets/[name]-[contenthash]'
) => {
  const code = (source as any).code ?? (source as any)
  const getHash = once(() => doHash(code))
  const hashWithLength = (length: number) => doHash(code, undefined, length)
  const query = name.split('?').pop()?.split('#', 1)[0]

  name = makeRelativeLegalPosixPath(
    name
      .split(/[\?\#]/, 1)[0]
      .split(':')
      .pop()!
  )

  const extname = getExtname(name)
  const basename = posixPath.basename(name, extname)
  const ext = extname.slice(1)
  const dirname = posixPath.dirname(name)

  if (typeof assetNames === 'function') {
    return normalizePath(
      assetNames({
        get hash() {
          return getHash()
        },
        get contentHash() {
          return getHash()
        },
        hashWithLength: hashWithLength,
        contentHashWithLength: hashWithLength,
        basename,
        query
      })
    )
  } else {
    let fileName = normalizePath(
      assetNames
        .replace(
          /\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*))?(?::(\d+))?\]/gi,
          (_, hashType: string, digestType: string, maxLength: string) => {
            return doHash(
              hashType?.toLowerCase() === 'hash' ? name : code,
              digestType?.toLowerCase() !== 'hex' ? 'base64url' : 'hex',
              parseInt(maxLength, 10) || undefined
            )
          }
        )
        .replace(/\[(\w*)\]/g, (_, ph: string) => {
          switch (ph.toLowerCase()) {
            case 'basename':
            case 'name':
              return basename
            case 'ext':
              return ext
            case 'extname':
              return extname
            case 'query':
              return query ?? ''
            case 'dir':
            case 'path':
            case 'folder':
              return dirname
            default:
              return ''
          }
        })
    )

    if (extname) {
      if (!fileName.endsWith(extname)) {
        fileName += extname
      }
    }

    return fileName
  }
}

export const getAssetFileName = (
  assetArgs: EmitAssetArgs,
  assetModuleFilename?: import('webpack').Compilation['outputOptions']['assetModuleFilename']
): string => {
  let { fileName, name, source } = assetArgs
  name = name || 'unknown'
  source = source || ''
  if (!fileName) {
    fileName = interpolateAssetName(
      name,
      isPromiseLike(source) ? performance.now() + name : source,
      assetModuleFilename
    )
  } else {
    fileName = makeRelativeLegalPosixPath(fileName)
  }

  return fileName
}

export const setWebpackAsset = (
  fileName: string,
  source: Awaitable<string | Uint8Array | { code: string | Uint8Array; map?: any }>,
  compilation: import('webpack').Compilation
) => {
  compilation.compiler.$compluginInjected.emittedAssets.set(fileName, source)
}

export const emitAsset = (assetArgs: EmitAssetArgs, compilation: import('webpack').Compilation) => {
  const fileName = getAssetFileName(assetArgs, compilation.outputOptions.assetModuleFilename)

  setWebpackAsset(fileName, assetArgs.source ?? '', compilation)

  return `(${WEBPACK_PUBLIC_PATH} + ${JSON.stringify('/' + fileName)})`
}

export const emitChunk = (
  request: string,
  compilation: import('webpack').Compilation,
  options: import('webpack').EntryOptions = {}
) => {
  const compiler = compilation.compiler
  const webpack = compiler.webpack
  const context = compilation.compiler.context

  const createDependency = (webpack.EntryPlugin?.createDependency ??
    (webpack.DynamicEntryPlugin as any)?.createDependency)!
  const resource = compiler.$compluginInjected.getRequestVirtualResource(request) ?? request

  let basename = makeRelativeLegalPosixPath(path.basename(resource.split(/[#\?]/, 1)[0]).split('.', 1)[0])
  const usedNames = compiler.$compluginInjected.usedChunkNames
  let name = basename
  let i = 0
  while (usedNames.has(name)) {
    name = basename + ++i
  }
  usedNames.add(name)

  return new Promise<string>((resolve, reject) => {
    let entryFileName = compilation.outputOptions.filename
    if (typeof entryFileName === 'string') {
      if (!entryFileName.includes('[')) {
        entryFileName = '[name]' + getExtname(entryFileName)
      }
    }
    const fileName = interpolateAssetName(name, name, entryFileName)

    const entryOptions: import('webpack').EntryOptions = {
      ...options,
      name: fileName
    }

    compilation.addEntry(context, createDependency(resource, entryOptions), entryOptions, (err, result) => {
      if (!result) {
        return reject(err)
      }

      resolve(`(${WEBPACK_PUBLIC_PATH} + ${JSON.stringify('/' + fileName)})`)
    })
  })
}

export const emitStatic = (fileName: string, compilation: import('webpack').Compilation) => {
  const staticFiles = compilation.compiler.$compluginInjected.staticFiles
  fileName = path.resolve(CWD, fileName)

  let result = staticFiles.get(fileName)
  if (!result) {
    staticFiles.set(
      fileName,
      (result = interpolateAssetName(fileName, fileName, compilation.outputOptions.assetModuleFilename))
    )
  }

  return result
}

export const doResolve = async (
  importee: string,
  importer: string | undefined,
  compilation: import('webpack').Compilation
) => {
  const injected = compilation.compiler.$compluginInjected
  return await injected.resolveId(importee, importer)
}

export const resolveWebpackExternals = (
  data: { context: string; request: string },
  externals: import('webpack').ExternalsPlugin['externals']
): Awaitable<boolean | null | undefined> => {
  if (externals == null) return
  if (typeof externals === 'string') {
    return data.request === externals
  } else if (externals instanceof RegExp) {
    return externals.test(data.request)
  } else if (isArray(externals)) {
    const promises: PromiseLike<boolean | null | undefined>[] = []
    for (const items of externals) {
      const result = resolveWebpackExternals(data, items)
      if (result != null) {
        if (isPromiseLike(result)) {
          promises.push(result)
        } else {
          return Boolean(result)
        }
      }
    }

    return Promise.all(promises).then(results => {
      for (const result of results) {
        if (result != null) {
          return Boolean(result)
        }
      }
    })
  } else if (typeof externals === 'function') {
    let result
    let isCallback = false

    const promise = new Promise<boolean | null | undefined>(async resolve => {
      const cb = (err: any, _result: any) => {
        result = _result
        isCallback = true
        resolve(result)
      }

      let awaitableResult
      if (externals.length >= 3) {
        awaitableResult = (externals as any)(data.context, data.request ?? (data as any).dependencies?.[0], cb)
      } else {
        awaitableResult = externals(data, cb)
      }

      if (isPromiseLike(awaitableResult)) {
        awaitableResult = await awaitableResult
      }

      if (!isCallback) {
        cb(undefined, awaitableResult)
      }
    })

    return isCallback ? result : promise
  } else if (typeof externals === 'object') {
    const { byLayer, ..._externals } = externals

    const { request } = data
    if (Object.prototype.hasOwnProperty.call(_externals, request)) {
      const external = _externals[request]
      return typeof external === 'string' ? true : Boolean(external)
    }
  }
}

const _ensureCompilerWebpack = (compiler: import('webpack').Compiler) => {
  if (compiler.webpack as any) return compiler.webpack

  let webpack
  try {
    if ('intermediateFileSystem' in compiler && 'cache' in compiler) {
      // webpack5
      webpack = esm_default(_require('webpack5'))
      if (!webpack.dependencies?.ConstDependency) {
        webpack.dependencies = {
          ...webpack.dependencies,
          ModuleDependency: esm_default(_require('webpack5/lib/dependencies/ModuleDependency')),
          ConstDependency: esm_default(_require('webpack5/lib/dependencies/ConstDependency')),
          NullDependency: esm_default(_require('webpack5/lib/dependencies/NullDependency'))
        }
      }
      if (!webpack.ExternalModule) {
        webpack.ExternalModule = esm_default(_require('webpack5/lib/ExternalModule'))
      }
    } else {
      // webpack4
      webpack = esm_default(_require('webpack4'))
      webpack.dependencies = {
        ...webpack.dependencies,
        ModuleDependency: esm_default(_require('webpack4/lib/dependencies/ModuleDependency')),
        ConstDependency: esm_default(_require('webpack4/lib/dependencies/ConstDependency')),
        NullDependency: esm_default(_require('webpack4/lib/dependencies/NullDependency'))
      }
      if (!webpack.ExternalModule) {
        webpack.ExternalModule = esm_default(_require('webpack4/lib/ExternalModule'))
      }
    }
  } catch {
    try {
      webpack = esm_default(loadDependency('webpack', true))
      if (!webpack.dependencies?.ConstDependency) {
        webpack.dependencies = {
          ...webpack.dependencies,
          ModuleDependency: esm_default(loadDependency('webpack/lib/dependencies/ModuleDependency', true)),
          ConstDependency: esm_default(loadDependency('webpack/lib/dependencies/ConstDependency', true)),
          NullDependency: esm_default(loadDependency('webpack/lib/dependencies/NullDependency', true))
        }
      }
      if (!webpack.ExternalModule) {
        webpack.ExternalModule = esm_default(loadDependency('webpack/lib/ExternalModule', true))
      }
    } catch {}
  }

  compiler.webpack = webpack
  return webpack as typeof import('webpack')
}

export const ensureCompilerWebpack = (compiler: import('webpack').Compiler) => {
  const webpack = _ensureCompilerWebpack(compiler)
  if (!webpack.EntryPlugin) {
    webpack.EntryPlugin = webpack.SingleEntryPlugin
  }
}
